﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Xml.Linq;
using AurelienRibon.Ui.Terminal;
using CodeIn.Core.Modules;
using CodeIn.Diagnostics;
using CodeIn.Utils;

namespace CodeIn.Core {
	public class ModuleManager {
		/// <summary>
		/// Returns the singleton instance.
		/// </summary>
		public static ModuleManager Instance { get { return instance; } }
		private static ModuleManager instance = new ModuleManager();

		/// <summary>
		/// Returns the list of every available modules.
		/// </summary>
		public List<BaseModule> RegisteredModules {
			get { return registeredModules; }
		}

		/// <summary>
		/// Returns the list of modules that are currently running.
		/// Last element is the most recently called module.
		/// </summary>
		public List<BaseModule> RunningModules {
			get { return runningModules;  }
		}

		/// <summary>
		/// Returns the latest running module, or null if no module is
		/// running.
		/// </summary>
		public BaseModule LatestRunningModule {
			get { return runningModules.Count > 0 ? runningModules[runningModules.Count - 1] : null;}
		}

		/// <summary>
		/// Returns the list of every available command.
		/// </summary>
		public List<string> RegisteredCommands {
			get { return registeredCommands; }
		}

		private readonly List<BaseModule> registeredModules;
		private readonly Dictionary<string, BaseModule> commandAssociationMap;
		private readonly List<BaseModule> runningModules;
		private readonly List<string> registeredCommands;

		// --------------------------------------------------------------------
		// Ctor
		// --------------------------------------------------------------------

		private ModuleManager() {
			registeredModules = new List<BaseModule>();
			commandAssociationMap = new Dictionary<string, BaseModule>();
			runningModules = new List<BaseModule>();
			registeredCommands = new List<string>();

			ModuleRegistered += (s, e) => {
				e.Module.ExecutionTestStarted += (ss, ee) => {
					RaiseModuleExecutionTestStarted(e.Module);
				};

				e.Module.ExecutionTestEnded += (ss, ee) => {
					RaiseModuleExecutionTestEnded(e.Module, ee.CanExecute);
				};

				e.Module.ExecutionStarted += (ss, ee) => {
					e.Module.ProgressUpdated += OnModuleProgressUpdated;
					lock (RunningModules) {
						RunningModules.Add(e.Module);
					}
					RaiseModuleExecutionStarted(e.Module);
				};

				e.Module.ExecutionEnded += (ss, ee) => {
					e.Module.ProgressUpdated -= OnModuleProgressUpdated;
					lock (RunningModules) {
						RunningModules.Remove(e.Module);
					}
					RaiseModuleExecutionEnded(e.Module);
				};
			};
		}

		private void OnModuleProgressUpdated(object sender, BaseModule.ProgressUpdatedEventArgs e) {
			RaiseModuleExecutionProgressed((BaseModule)sender, e.Percents);
		}

		// --------------------------------------------------------------------
		// Public API
		// --------------------------------------------------------------------

		/// <summary>
		/// Triggers a raw call in the wild, hoping for something to catch it
		/// (usually a terminal, which will prompt the command and trigger
		/// an async call in return).
		/// </summary>
		public void CallRaw(string rawCommand) {
			RaiseRawCallTriggered(rawCommand);
		}

		/// <summary>
		/// Synchronously invokes the SafeExecute method of the module 
		/// corresponding to the given command, once parsed.
		/// </summary>
		/// <exception cref="ModuleManager.ModuleNotFoundException"/>
		public void CallSync(string rawCommand) {
			var cmd = TerminalUtils.ParseCommandLine(rawCommand);
			CallSync(cmd.Name, cmd.Args);
		}

		/// <summary>
		/// Synchronously invokes the SafeExecute method of the module 
		/// corresponding to the given command. In arguments array, arg[0] must be
		/// the first argument, not the command name.
		/// </summary>
		/// <exception cref="ModuleManager.ModuleNotFoundException"/>
		public void CallSync(string command, string[] args) {
			// Creates the arguments that will be given to the module.
			// First argument is the command name.
			string[] moduleArgs = new string[1 + args.Length];
			moduleArgs[0] = command;
			args.CopyTo(moduleArgs, 1);

			// Calls the module.
			BaseModule calledModule = GetModuleFromCommand(command);
			calledModule.SafeExecute(moduleArgs);
		}

		/// <summary>
		/// Asynchronously invokes the SafeExecute method of the module 
		/// corresponding to the given command, once parsed.
		/// </summary>
		/// <exception cref="ModuleManager.ModuleNotFoundException"/>
		public void CallAsync(string rawCommand) {
			var cmd = TerminalUtils.ParseCommandLine(rawCommand);
			CallAsync(cmd.Name, cmd.Args);
		}

		/// <summary>
		/// Asynchronously invokes the SafeExecute method of the module 
		/// corresponding to the given command. In arguments array, arg[0] must be
		/// the first argument, not the command name.
		/// <exception cref="ModuleManager.ModuleNotFoundException"/>
		public void CallAsync(string command, string[] args) {
			// Creates the arguments that will be given to the module.
			// First argument is the command name.
			string[] moduleArgs = new string[1 + args.Length];
			moduleArgs[0] = command;
			args.CopyTo(moduleArgs, 1);

			// Looks for the requested module.
			BaseModule calledModule = GetModuleFromCommand(command);

			// Loads the module asynchronously.
			ThreadPool.QueueUserWorkItem(p => {
				calledModule.SafeExecute(moduleArgs);
			});
		}

		/// <summary>
		/// Calls the Initialize() method of every loaded modules.
		/// </summary>
		public void InitiliazeModules() {
			foreach (var mod in registeredModules)
				mod.Initialize();
		}

		/// <summary>
		/// Looks for module definitions in the resources/modules/definitions folder.
		/// </summary>
		public void LoadModules() {
			// Gets every embedded module definition
			var resources = AssemblyUtils.GetResources("resources/modules/definitions/(.+?).xml$");

			// Defines module lists.
			List<Tuple<string, string>> moduleNotFound = new List<Tuple<string, string>>();

			// Parses found module definitions
			foreach (var res in resources) {
				StreamReader sr = new StreamReader(res.Value);
				XDocument xdoc = XDocument.Parse(sr.ReadToEnd());

				// Tests the minimal compliance of the description
				if (xdoc.Element("Module") == null) {
					string msg = string.Format("'{0}' tag was not found in {1}", "Module", res.Key);
					MessageHandlers.Debug.WriteErrorLine(msg);
					continue;
				}
				if (xdoc.Element("Module").Element("Name") == null) {
					string msg = string.Format("'{0}' tag was not found in {1}", "Module.Name", res.Key);
					MessageHandlers.Debug.WriteErrorLine(msg);
					continue;
				}
				if (xdoc.Element("Module").Element("Type") == null) {
					string msg = string.Format("'{0}' tag was not found in {1}", "Module.Type", res.Key);
					MessageHandlers.Debug.WriteErrorLine(msg);
					continue;
				}

				// Retrieves all elements from the description
				XElement m = xdoc.Element("Module");

				string name = m.Element("Name").Value.Trim();
				string type = m.Element("Type").Value.Trim();
				string revision = m.Element("Revision") != null ? m.Element("Revision").Value.Trim() : "?";
				string author = m.Element("Author") != null ? m.Element("Author").Value.Trim() : "?";
				string command = m.Element("Commands") != null ? m.Element("Commands").Value.Trim() : null;
				string[] commands = null;
				if (command != null) {
					commands = command.Split(new char[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries);
					for (int i = 0; i < commands.Length; i++)
						commands[i] = commands[i].Trim();
				}
				string sdesc = m.Element("ShortDescription") != null ? m.Element("ShortDescription").Value.Trim() : "";
				string ldesc = m.Element("LongDescription") != null ? m.Element("LongDescription").Value.Trim() : "";

				// Finds and invokes the constructor of the module.
				Type t = Type.GetType(type);
				if (t != null) {
					ConstructorInfo ctrInfo = t.GetConstructor(Type.EmptyTypes);
					BaseModule module = (BaseModule)ctrInfo.Invoke(null);
					module.Infos = new ModuleInfo(name, t, revision, author, commands, sdesc, ldesc);
					registeredModules.Add(module);
					foreach (var cmd in commands) {
						if (registeredCommands.Contains(cmd)) {
							throw new CommandAlreadyRegisteredException(cmd);
						} else {
							registeredCommands.Add(cmd);
							commandAssociationMap.Add(cmd, module);
						}
					}
				} else {
					moduleNotFound.Add(new Tuple<string,string>(name, type));
				}
			}

			// Sort modules.
			registeredModules.Sort((m1, m2) => { return m1.Infos.Name.CompareTo(m2.Infos.Name); });

			// Raise events.
			foreach (var mod in registeredModules) {
				RaiseModuleRegistered(mod);
			}

			// Outputs infos.
			foreach (var mod in registeredModules) {
				string msg = string.Format("Module found: {0} (rev {1}, © {2})", 
					mod.Infos.Name, mod.Infos.Revision, mod.Infos.Author);
				MessageHandlers.Debug.WriteLine(msg);
			}
			foreach (var infos in moduleNotFound) {
				string msg = string.Format("Module not found: {0} ({1})", infos.Item1, infos.Item2);
				MessageHandlers.Debug.WriteErrorLine(msg);
			}
		}

		/// <summary>
		/// Reads the startup sequence and launches the commands.
		/// </summary>
		public void LoadStartupSequence() {
			Uri uri = new Uri("pack://application:,,,/resources/modules/StartupSequence.txt");
			var stream = App.GetResourceStream(uri).Stream;
			StreamReader sr = new StreamReader(stream);

			string[] sequence = sr.ReadToEnd().Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
			foreach (var line in sequence) {
				var cmd = TerminalUtils.ParseCommandLine(line);
				try {
					MessageHandlers.Debug.WriteLine("Startup execution of '" + cmd.Name + "'.");
					CallSync(cmd.Name, cmd.Args);
				} catch (ModuleNotFoundException ex) {
					MessageHandlers.Debug.WriteWarningLine("Startup command '" + ex.CommandName + "' was not found.");
				}
			}
		}

		/// <summary>
		/// Returns a BaseModule associated to the given command, or thow an exception if
		/// not found. Command search is case-insensitive.
		/// </summary>
		/// <exception cref="ModuleManager.ModuleNotFoundException"/>
		public BaseModule GetModuleFromCommand(string command) {
			BaseModule calledModule = null;
			foreach (var mod in commandAssociationMap)
				if (mod.Key.Equals(command, StringComparison.InvariantCultureIgnoreCase))
					calledModule = mod.Value;

			if (calledModule == null)
				throw new ModuleNotFoundException(command);

			return calledModule;
		}

		// --------------------------------------------------------------------
		// Events
		// --------------------------------------------------------------------

		public event EventHandler<ModuleRegistrationEventArgs> ModuleRegistered;
		private void RaiseModuleRegistered(BaseModule module) {
			if (ModuleRegistered != null)
				ModuleRegistered(this, new ModuleRegistrationEventArgs(module));
		}

		public event EventHandler<ModuleExecutionEventArgs> ModuleExecutionTestStarted;
		private void RaiseModuleExecutionTestStarted(BaseModule module) {
			if (ModuleExecutionTestStarted != null)
				ModuleExecutionTestStarted(this, new ModuleExecutionEventArgs(module));
		}

		public event EventHandler<ModuleExecutionTestResultEventArgs> ModuleExecutionTestEnded;
		private void RaiseModuleExecutionTestEnded(BaseModule module, bool result) {
			if (ModuleExecutionTestEnded != null)
				ModuleExecutionTestEnded(this, new ModuleExecutionTestResultEventArgs(module, result));
		}

		public event EventHandler<ModuleExecutionEventArgs> ModuleExecutionStarted;
		private void RaiseModuleExecutionStarted(BaseModule module) {
			if (ModuleExecutionStarted != null)
				ModuleExecutionStarted(this, new ModuleExecutionEventArgs(module));
		}

		public event EventHandler<ModuleExecutionProgressedEventArgs> ModuleExecutionProgressed;
		private void RaiseModuleExecutionProgressed(BaseModule module, int progress) {
			if (ModuleExecutionProgressed != null)
				ModuleExecutionProgressed(this, new ModuleExecutionProgressedEventArgs(module, progress));
		}

		public event EventHandler<ModuleExecutionEventArgs> ModuleExecutionEnded;
		private void RaiseModuleExecutionEnded(BaseModule module) {
			if (ModuleExecutionEnded != null)
				ModuleExecutionEnded(this, new ModuleExecutionEventArgs(module));
		}

		public event EventHandler<RawCallTriggeredEventArgs> RawCallTriggered;
		private void RaiseRawCallTriggered(string rawCommand) {
			if (RawCallTriggered != null)
				RawCallTriggered(this, new RawCallTriggeredEventArgs(rawCommand));
		}

		// --------------------------------------------------------------------

		public class ModuleRegistrationEventArgs : EventArgs {
			public BaseModule Module { get; private set; }
			public ModuleRegistrationEventArgs(BaseModule module) {
				Module = module;
			}
		}

		public class ModuleExecutionTestResultEventArgs : EventArgs {
			public BaseModule Module { get; private set; }
			public bool CanExecute { get; private set; }
			public ModuleExecutionTestResultEventArgs(BaseModule module, bool result) {
				Module = module;
				CanExecute = result;
			}
		}

		public class ModuleExecutionEventArgs : EventArgs {
			public BaseModule Module { get; private set; }
			public ModuleExecutionEventArgs(BaseModule module) {
				Module = module;
			}
		}

		public class ModuleExecutionProgressedEventArgs : EventArgs {
			public BaseModule Module { get; private set; }
			public int Progress { get; private set; }
			public ModuleExecutionProgressedEventArgs(BaseModule module, int progress) {
				Module = module;
				Progress = progress;
			}
		}

		public class RawCallTriggeredEventArgs : EventArgs {
			public string RawCommand {get; private set;}
			public RawCallTriggeredEventArgs(string rawCommand) {
				RawCommand = rawCommand;
			}
		}

		// --------------------------------------------------------------------
		// Exceptions
		// --------------------------------------------------------------------

		public class ModuleNotFoundException : Exception {
			public string CommandName { get; private set; }
			public ModuleNotFoundException(string commandName) {
				CommandName = commandName;
			}
		}

		public class CommandAlreadyRegisteredException : Exception {
			public string CommandName { get; private set; }
			public CommandAlreadyRegisteredException(string commandName) {
				CommandName = commandName;
			}
		}
	}
}
